﻿using log4net;
using Newtonsoft.Json.Linq;
using per.esmool.utils;
using System;
using System.Collections.Generic;
using System.Linq;

namespace FlowSimulate {

    public class Director {

        private static readonly ILog LOG = LogManager.GetLogger(typeof(Director));

        readonly Dictionary<string, Asset> assets;
        readonly Dictionary<string, List<string>> positionAssets;

        public WorkDateKit WorkDateKit { get; set; }

        public Suit Suit { get; set; }

        public Asset GetAsset(string key) {
            return this.assets[key];
        }

        public List<Asset> GetAssetByStageKey(string stageKey) {
            if (!this.positionAssets.ContainsKey(stageKey))
                return null;
            var qs = this.positionAssets[stageKey];
            var ps = from key in qs
                     select this.assets[key];
            return ps.ToList();
        }

        public Director(string assetFile) {
            var data = YamlKit.ParseFile<JArray>(assetFile);
            var qs = from item in data
                     let json = item as JObject
                     select new Asset {
                         Key = json.Value<string>("key"),
                         SN = json.Value<string>("sn"),
                         Position = json.Value<string>("position"),
                         Name = json.Value<string>("name")
                     };
            this.assets = qs.ToDictionary(x => x.Key);
            this.positionAssets = qs.GroupBy(x => x.Position).ToDictionary(g => g.Key, g => g.Select(x => x.Key).ToList());
        }

        public List<ArrangedTask> Arrange(List<Task> tasks, DateTime startDate) {
            var pendingTasks = new Queue<Task>(tasks);
            var workingTasks = new Queue<ArrangedTask>();
            var doneTasks = new Dictionary<int, ArrangedTask>();

            DateTime today = startDate - TimeSpan.FromDays(1);

            while (pendingTasks.Count > 0 && workingTasks.Count > 0) {
                today = this.WorkDateKit.NextDay(today);
                var durties = new Dictionary<string, AssetDurty>();
                LOG.Info($"---------- Work Date {today:yyyy-MM-dd} ----------");

                this.CheckWorkingTasks(today, workingTasks, doneTasks, durties);

                this.CheckPendingTasks(today, pendingTasks, workingTasks, doneTasks, durties);
            }

            return [.. doneTasks.Values];
        }

        private void CheckWorkingTasks(DateTime today,
            Queue<ArrangedTask> workingTasks,
            Dictionary<int, ArrangedTask> doneTasks, 
            Dictionary<string, AssetDurty> durties
        ) {
            LOG.Debug($"Checking working tasks");
            int count = workingTasks.Count;
            for (int i = 0; i < count; i++) {
                var task = workingTasks.Dequeue();
                var assetKey = task.Asset.Key;

                if (task.ResourceRequired <= this.WorkDateKit.DurtyTimeOfDay) {
                    task.EndDate = this.WorkDateKit.AddWorkMinutes(today, task.ResourceRequired);
                    this.CostAsset(durties, assetKey, task.ResourceRequired);
                    task.ResourceRequired = 0;
                    doneTasks.Add(task.ID, task);
                    LOG.Info($"Task {task.ID} done");
                    continue;
                }

                task.ResourceRequired -= this.WorkDateKit.DurtyTimeOfDay;
                this.CostAsset(durties, assetKey, this.WorkDateKit.DurtyTimeOfDay);
                workingTasks.Enqueue(task);
                LOG.Debug($"Task {task.ID} costs 1.0 work labor");
            }
        }

        private void CheckPendingTasks(
            DateTime today,
            Queue<Task> pendingTasks,
            Queue<ArrangedTask> workingTasks,
            Dictionary<int, ArrangedTask> doneTasks,
            Dictionary<string, AssetDurty> durties
        ) {
            LOG.Debug($"Checking pending tasks");
            int pendingCount = pendingTasks.Count;
            for (int i=0; i<pendingCount; i++) {
                var task = pendingTasks.Dequeue();
                var arranged = this.ArrangeTask(today, task, doneTasks, durties);
                if (arranged is null) {
                    pendingTasks.Enqueue(task);
                    continue;
                }

                if (arranged.ResourceRequired > 0) {
                    workingTasks.Enqueue(arranged);
                    continue;
                }

                doneTasks.Add(arranged.ID, arranged);
                LOG.Info($"Task {task.ID} done on starting date");
            }
        }

        private void CostAsset(Dictionary<string, AssetDurty> durties, string assetKey, int workMinutes) {
            if (durties.ContainsKey(assetKey)) {
                durties[assetKey].Cost(workMinutes);
                return;
            }

            durties[assetKey] = new AssetDurty {
                AvailableWorkMinutes = this.WorkDateKit.DurtyTimeOfDay - workMinutes,
            };
        }

        private List<Tuple<string, int>> GetAvaliableAssets(string stage, Dictionary<string, AssetDurty> durties) {
            var assets = this.positionAssets[stage];
            var list = new List<Tuple<string, int>>();
            foreach (var asset in assets) {
                if (!durties.ContainsKey(asset)) {
                    list.Add(Tuple.Create(asset, this.WorkDateKit.DurtyTimeOfDay));
                    continue;
                }

                var durty = durties[asset];
                if (durty.AvailableWorkMinutes <= 0)
                    continue;

                list.Add(Tuple.Create(asset, durty.AvailableWorkMinutes));
            }
            list.Sort((x, y) => x.Item2 - y.Item2);
            return list;
        }

        private ArrangedTask ArrangeTask(
            DateTime today, 
            Task task, 
            Dictionary<int, ArrangedTask> doneTasks, 
            Dictionary<string, AssetDurty> durties
        ) {
            var predecessorsAllDone = task.After.All(doneTasks.ContainsKey);
            var availableAssets = this.GetAvaliableAssets(task.Stage, durties);
            if (availableAssets.Count == 0)
                return null;

            var availableAsset = availableAssets.First();
            var availableMinutes = availableAsset.Item2;
            var requiredMinutes = (int)(this.WorkDateKit.DurtyTimeOfDay * task.Cost);
            var remainRequiredMinutes = requiredMinutes < availableMinutes ? 0 : requiredMinutes - availableMinutes;
            var costMinutes = requiredMinutes < availableMinutes ? requiredMinutes : availableMinutes;
            var assetStartTime = this.WorkDateKit.AddWorkMinutes(today, availableAsset.Item2);
            var assetEndTime = remainRequiredMinutes > 0 ? (DateTime?)null : this.WorkDateKit.AddWorkMinutes(assetStartTime, costMinutes);
            var asset = this.assets[availableAsset.Item1];

            var arranged = new ArrangedTask(task) {
                StartDate = assetStartTime,
                EndDate = assetEndTime,
                ResourceRequired = remainRequiredMinutes,
                Asset = asset
            };
            LOG.Info($"Task {task.ID} starts, assigned to asset {asset.Key}, requiring {task.Cost:0.0} work labor");
            return arranged;
        }
    }

    public class Asset {

        public Asset() { }

        public Asset(Asset from) {
            this.Key = from.Key;
            this.SN = from.SN;
            this.Name = from.Name;
            this.Position = from.Position;
        }

        public string Key { get; set; }
        public string SN { get; set; }
        public string Name { get; set; }
        public string Position { get; set; }

        public string ID => $"{this.SN}{this.Key}";

    }

    public class AssetDurty {

        public string Key { get; set; }
        public int AvailableWorkMinutes { get; set; }

        public void Cost(int workMinutes) {
            this.AvailableWorkMinutes -= workMinutes;
        }

    }

    public class ArrangedTask : Task {

        public ArrangedTask(ArrangedTask from) : base(from) {
            this.StartDate = from.StartDate;
            this.EndDate = from.EndDate;
            this.Asset = new Asset(from.Asset);
            this.ResourceRequired = from.ResourceRequired;
            this.OpenedBy = from.OpenedBy;
            this.FinishedBy = from.FinishedBy;
            this.ClosedBy = from.ClosedBy;
            this.LastEditedBy = from.LastEditedBy;
            this.LastEditDate = from.LastEditDate;
        }

        public ArrangedTask(Task from) : base(from) { }

        public DateTime StartDate { get; set; }
        public DateTime? EndDate { get; set; }
        public DateTime? AssignDate { get; set; }
        public DateTime? FinishDate { get; set; }
        public Asset Asset { get; set; }
        public int ResourceRequired { get; set; }

        public string OpenedBy { get; set; }
        public string FinishedBy { get; set; }
        public string ClosedBy { get; set; }
        public string LastEditedBy { get; set; }
        public DateTime? LastEditDate { get; set; }

        public void Init(int workMinutesOfDay) {
            this.ResourceRequired = (int)(this.Cost * workMinutesOfDay);
        }
    }
}